Le code source de cet exemple peut être téléchargé ici.
Nous avons réalisé dans le tutoriel "Tutoriel REPL Haskeline" une boucle REPL avec une ligne de commandes assez complète avec Haskeline.
Néanmoins, deux petits problèmes subsistent.
L'enregistrement de l'historique des commande ne se fait que lorsque le programme se termine correctement et cela peut être problématique si on veut pouvoir visualiser les commandes introduites après un crash du programme.
Le fichier d'historique contient les commandes dans l'ordre allant de la plus récente à la plus ancienne, ce qui est assez contre intuitif.
Nous allons donc voir comment utiliser certaines fonctions "bas niveau" de la bibliothèque Haskeline afin de corriger ces deux défauts.
Seulement … voila ! Ces fonctions sont faites pour fonctionner à l'intérieur de la monade InputT
, il va donc falloir d'abord réécrire notre programme pour le faire tourner au-dessus de la monade InputT
.
La conversion de notre programme est très facile, il suffit de lancer la monade InputT
avec la fonction runInputT
avec comme arguments nos préférences et la fonction principale.
main = do
runInputT mySettings $ do
outputStrLn $ unlines help
replLoop 1
Il faut ensuite utiliser la fonction liftIO
du module Control.Monad.Trans
pour pouvoir lancer des fonctions d'entrée sortie de la monade IO
. Les fonctions putStrLn
peuvent être remplacées par la fonction outputStrLn
de la bibliothèque Haskeline.
replEval com@(':' : 'l' : 's' : _ ) = do
dir <- liftIO getCurrentDirectory
content <- liftIO $ getDirectoryContents dir
let filteredContent = sort $ filter (\f -> notElem f [".", ".."]) content
return filteredContent
replEval com@(':' : 'h' : 'e' : 'u' : 'r' : 'e' : _ ) = do
tim <- liftIO getCurrentTime
zone <- liftIO getCurrentTimeZone
let (TimeOfDay h m s) = localTimeOfDay $ utcToLocalTime zone tim
return ["Il est " ++ show h ++ " heures " ++ show m ++ " minutes " ++ show s ++ " secondes"]
Notre programme est maintenant prêt pour les modifications que l'on veut apporter.
Pour commencer nous allons désactiver la création automatique de l'historique dans les préférences de la ligne de commande.
mySettings = Settings
{ complete = completeWord Nothing "" searchFunc
, historyFile = Nothing
, autoAddHistory = True
}
Pour récupérer le fichier d'historique, on commence par vérifier son existence avec la fonction doesFileExist
du module System.Directory
.
Si le fichier n'existe pas, on définit un historique vide emptyHistory
de la monade avec la fonction putHistory
.
Si le fichier existe, on lit le fichier avec readFile
et on le décompose en lignes avec lines
. On effectue ensuite des ajouts successifs à un historique vide avec addHistory
et foldr
avec la liste inversée des lignes du fichier d'historique. On définit ensuite l'historique de la monade avec putHistory
.
main = do
runInputT mySettings $ do
ex <- liftIO $ doesFileExist "history.hist"
if ex
then do
lns <- liftIO $ lines <$> readFile "history.hist"
let hist = foldr addHistory emptyHistory (reverse lns)
putHistory hist
else do
putHistory emptyHistory
outputStrLn $ unlines help
replLoop 1
Pour enregistrer les commandes dans le fichier d'historique, on fait un ajout dans celui-ci avec appendFile
après chaque invocation de getInputLine
. On n'oublie pas d'ajouter un retour à la ligne à chaque fois si on ne veut pas se retrouver avec un fichier d'une seule ligne.
replRead i = do
com <- getInputLine ("ma commande " ++ show i ++ " >")
case com of
Just c -> liftIO $ appendFile "history.hist" (c ++ "\n")
Nothing -> return ()
return com
Lorsque l'on lance le programme et que l'on commence à taper des commandes, on constate que le programme plante avec un message d'erreur du type:
REPL3_fr: history.hist: openFile: resource busy (file is locked)
Ce problème est lié au fonctionnement de Haskell et plus particulièrement à l'évaluation paresseuse (lazy).
En effet, Haskell ne lit pas le fichier historique au début du programme mais seulement lorsqu'il en a besoin. C'est à dire, dans le cas présent, lorsque l'on veut y accéder pour écrire une ligne. Ce qui cause une erreur fatale.
Pour éviter cela, plusieurs solutions peuvent être envisagées:
Utiliser une version stricte de readFile
, mais elle n'est pas toujours disponible.
Forcer la lecture du fichier au début du programme en effectuant une opération "bidon" sur le contenu du fichier.
On peut par exemple calculer le nombre de lignes contenu dans le fichier avec evaluate
du module Control.Exception
et ne pas utiliser le résultat.
_ <- liftIO $ evaluate (length lns)
Ou alors profiter de la lecture du fichier pour afficher un message d'information sur le nombre de lignes récupérées du fichier.
outputStrLn $ "Chargement de " ++ show (length lns) ++ " lignes d'historique du fichier : "
Cette solution étant la plus facile à mettre en oeuvre.
main = do
runInputT mySettings $ do
ex <- liftIO $ doesFileExist "history.hist"
if ex
then do
lns <- liftIO $ lines <$> readFile "history.hist"
let hist = foldr addHistory emptyHistory (reverse lns)
-- ~ _ <- liftIO $ evaluate (length lns)
outputStrLn $ "Chargement de " ++ show (length lns) ++ " lignes d'historique du fichier : "++ "history.hist"
putHistory hist
else do
putHistory emptyHistory
outputStrLn $ unlines help
replLoop 1
Et maintenant tout fonctionne parfaitement.
Voilà, c'est fini ! J'espère que ces petits tutoriels sur Haskeline et les boucles REPL vous seront utiles et vous permettront de développer de beaux programme en ligne de commandes.